Skip to content

feat: support v2/v3/non-v1 API version prefix in upstream channel baseURL - #5348

Open
waxiangzi wants to merge 2 commits into
QuantumNous:mainfrom
waxiangzi:compat/protocol-v2-v3
Open

feat: support v2/v3/non-v1 API version prefix in upstream channel baseURL#5348
waxiangzi wants to merge 2 commits into
QuantumNous:mainfrom
waxiangzi:compat/protocol-v2-v3

Conversation

@waxiangzi

@waxiangzi waxiangzi commented Jun 6, 2026

Copy link
Copy Markdown

Problem

When Base URL contains a version prefix or custom path (e.g. https://example.com/v2, https://open.bigmodel.cn/api/coding/paas/v4, https://api.lkeap.cloud.tencent.com/plan/v3), the system hardcodes /v1 into the upstream URL, causing duplication and 404 errors.

For example:

  • Base URL = https://example.com/v2
  • Actual upstream URL becomes: https://example.com/v2/v1/chat/completions

Solution

1. Normalize version prefix at the router level (relay/constant/relay_mode.go)

Add normalizeVersionPrefix(): any incoming path like /v2/chat/completions or /v1beta/models/... is normalized to /v1/... for internal routing, so the rest of the relay pipeline works unchanged.

2. Auto-detect version prefix in GetFullRequestURL (relay/common/relay_utils.go)

  • BaseUrlHasVersionPrefix(): parses baseURL path segments to detect vN... prefixes.
  • StripVersionPrefix(): strips the /v1 prefix from the client request path before appending to baseURL, preventing duplication when baseURL already contains a version.
  • Cloudflare gateway special case preserved.

3. Remove hardcoded /v1/ constructions in Controller layer

  • controller/channel.go: model list fetching
  • controller/channel_upstream_update.go: upstream sync (volcengine/moonshot/default)
  • controller/channel-billing.go: billing endpoints
  • controller/video_proxy.go: video proxy

All replaced with GetFullRequestURL(...).

4. Remove hardcoded /v1/ in OpenAI-compat Adaptor layer

  • relay/channel/openai/adaptor.go: Claude/Gemini conversion branch now extracts version prefix from baseURL automatically
  • relay/channel/moonshot/adaptor.go
  • relay/channel/perplexity/adaptor.go
  • relay/channel/siliconflow/adaptor.go
  • relay/channel/deepseek/adaptor.go

Usage Example

Configure Base URL with the full upstream API prefix:

Channel Base URL
OpenAI-compat v2 https://example.com/v2
OpenAI-compat v3 https://example.com/api/v3
Standard OpenAI https://api.openai.com/v1

Clients continue sending standard /v1/chat/completions to new-api; the gateway automatically adjusts the upstream URL based on the channel's Base URL.

Summary by CodeRabbit

  • Refactor
    • Standardized upstream request URL building across multiple provider channels using shared URL helpers for models, chat/completions, responses, embeddings, rerank, and video content.
    • Improved handling of versioned base URLs and API path prefixes by detecting and stripping version prefix segments, improving compatibility with providers using versioned routes.
    • Enhanced relay-mode detection by normalizing versioned paths before matching endpoints.

- Add BaseUrlHasVersionPrefix/StripVersionPrefix helpers in relay/common
- Auto-strip /v1 from requestURL when baseURL already contains version prefix
- Normalize version prefix in path-to-relay-mode matching
- Update channel billing, models fetch, upstream sync, video proxy to use GetFullRequestURL
- Update deepseek, moonshot, openai, perplexity, siliconflow adaptors
@coderabbitai

coderabbitai Bot commented Jun 6, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d00572c5-dba5-4962-9268-9d9384909e0f

📥 Commits

Reviewing files that changed from the base of the PR and between bb256b8 and ac2b978.

📒 Files selected for processing (6)
  • controller/channel.go
  • controller/channel_upstream_update.go
  • controller/video_proxy.go
  • relay/channel/moonshot/adaptor.go
  • relay/channel/openai/adaptor.go
  • relay/common/relay_utils.go
🚧 Files skipped from review as they are similar to previous changes (5)
  • relay/channel/openai/adaptor.go
  • controller/video_proxy.go
  • controller/channel.go
  • relay/channel/moonshot/adaptor.go
  • relay/common/relay_utils.go

Walkthrough

This PR centralizes request URL construction through version-aware helpers, updates relay-mode path normalization, and migrates controller and relay adaptor endpoints from direct string concatenation to shared URL generation.

Changes

URL Building Centralization

Layer / File(s) Summary
Core relay utilities and path normalization
relay/common/relay_utils.go, relay/constant/relay_mode.go
Adds version-prefix detection and stripping helpers, updates gateway and versioned-base URL handling, and normalizes versioned paths before relay-mode detection.
Controller URL construction migration
controller/channel-billing.go, controller/channel.go, controller/channel_upstream_update.go, controller/video_proxy.go
Migrates billing, model, upstream discovery, and video content URLs to relaycommon.GetFullRequestURL().
Relay adaptor URL construction migration
relay/channel/deepseek/adaptor.go, relay/channel/moonshot/adaptor.go, relay/channel/openai/adaptor.go, relay/channel/perplexity/adaptor.go, relay/channel/siliconflow/adaptor.go
Migrates endpoint construction for completions, chat, rerank, embeddings, responses, and Azure task paths to shared helpers.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related PRs

Poem

🐰 Paths hop neatly, side by side,

One helper guides the API ride.
Versions tuck their prefixes in,
Gateway routes now cleanly spin!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: supporting non-v1 API version prefixes in upstream base URLs.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@controller/video_proxy.go`:
- Around line 110-113: The follow-up fetch for OpenAI/Sora videos uses the live
channel.Key which can break if keys rotate; update the logic in the case
handling (where relaycommon.GetFullRequestURL and task.GetUpstreamTaskID are
used) to prefer the submission-captured key stored on the task
(task.PrivateData.Key) falling back to channel.Key, and apply any stored
resolved header overrides from task.PrivateData (e.g., Authorization or other
headers) to req.Header before sending the request so the request reuses the
exact auth context captured at submission time.

In `@relay/common/relay_utils.go`:
- Around line 43-57: The function StripVersionPrefix is dropping the leading
slash when removing the version (it currently returns path[secondSlash+1:]), so
update the return in StripVersionPrefix to return path[secondSlash:] instead
(preserving the leading '/'); locate the return inside the version-detection
branch in StripVersionPrefix and replace the slice expression accordingly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 9f06659d-2fee-46b2-b5b2-f50532a2ba4f

📥 Commits

Reviewing files that changed from the base of the PR and between 4ca47ee and bb256b8.

📒 Files selected for processing (11)
  • controller/channel-billing.go
  • controller/channel.go
  • controller/channel_upstream_update.go
  • controller/video_proxy.go
  • relay/channel/deepseek/adaptor.go
  • relay/channel/moonshot/adaptor.go
  • relay/channel/openai/adaptor.go
  • relay/channel/perplexity/adaptor.go
  • relay/channel/siliconflow/adaptor.go
  • relay/common/relay_utils.go
  • relay/constant/relay_mode.go

Comment thread controller/video_proxy.go
Comment on lines 110 to 113
case constant.ChannelTypeOpenAI, constant.ChannelTypeSora:
videoURL = fmt.Sprintf("%s/v1/videos/%s/content", baseURL, task.GetUpstreamTaskID())
videoURL = relaycommon.GetFullRequestURL(baseURL, fmt.Sprintf("/v1/videos/%s/content", task.GetUpstreamTaskID()), channel.Type)
req.Header.Set("Authorization", "Bearer "+channel.Key)
default:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Reuse task-captured auth for OpenAI/Sora content fetches

Line 112 still uses channel.Key for the follow-up request. If channel keys rotate (or multi-key selection changes) after submission, completed tasks can fail to download with upstream auth errors. Prefer task.PrivateData.Key first (fallback to channel.Key), and apply stored resolved header overrides when available.

💡 Suggested fix (key selection)
 case constant.ChannelTypeOpenAI, constant.ChannelTypeSora:
 	videoURL = relaycommon.GetFullRequestURL(baseURL, fmt.Sprintf("/v1/videos/%s/content", task.GetUpstreamTaskID()), channel.Type)
-	req.Header.Set("Authorization", "Bearer "+channel.Key)
+	apiKey := strings.TrimSpace(task.PrivateData.Key)
+	if apiKey == "" {
+		apiKey = strings.TrimSpace(channel.Key)
+	}
+	req.Header.Set("Authorization", "Bearer "+apiKey)

Based on learnings, async video follow-up requests should reuse authentication context captured at submission time (including task-stored key and resolved header overrides).

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
case constant.ChannelTypeOpenAI, constant.ChannelTypeSora:
videoURL = fmt.Sprintf("%s/v1/videos/%s/content", baseURL, task.GetUpstreamTaskID())
videoURL = relaycommon.GetFullRequestURL(baseURL, fmt.Sprintf("/v1/videos/%s/content", task.GetUpstreamTaskID()), channel.Type)
req.Header.Set("Authorization", "Bearer "+channel.Key)
default:
case constant.ChannelTypeOpenAI, constant.ChannelTypeSora:
videoURL = relaycommon.GetFullRequestURL(baseURL, fmt.Sprintf("/v1/videos/%s/content", task.GetUpstreamTaskID()), channel.Type)
apiKey := strings.TrimSpace(task.PrivateData.Key)
if apiKey == "" {
apiKey = strings.TrimSpace(channel.Key)
}
req.Header.Set("Authorization", "Bearer "+apiKey)
default:
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@controller/video_proxy.go` around lines 110 - 113, The follow-up fetch for
OpenAI/Sora videos uses the live channel.Key which can break if keys rotate;
update the logic in the case handling (where relaycommon.GetFullRequestURL and
task.GetUpstreamTaskID are used) to prefer the submission-captured key stored on
the task (task.PrivateData.Key) falling back to channel.Key, and apply any
stored resolved header overrides from task.PrivateData (e.g., Authorization or
other headers) to req.Header before sending the request so the request reuses
the exact auth context captured at submission time.

Source: Learnings

Comment thread relay/common/relay_utils.go
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant